package com.hmdp.utils;

import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;

/**
 * Redis工具类
 *
 * @author 24040
 * @name CacheClient
 * @date 2022-09-11 10:08
 */
@Slf4j
@Component
public class CacheClient<T> {
    private final StringRedisTemplate stringRedisTemplate;

    public CacheClient(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 将任意Java对象序列化为json并存储在string类型的key中，
     * 并且可以设置TTL过期时间
     *
     * @param key   redis中的key值
     * @param value value值
     * @param time  过期时间
     * @param unit  过期单位
     */
    public void set(String key, Object value, Long time, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    }

    /**
     * 将任意Java对象序列化为json并存储在string类型的key中，并且可以设置逻辑过期时间，用于处理缓存击穿问题
     * @param key   redis中的key值
     * @param value value值
     * @param time  过期时间
     * @param unit  过期单位
     */
    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
        // 设置逻辑过期
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));//unit.toSeconds会将时间进行转换，转换位秒
        //写入redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

    /**
     * 从redis中取出数据，并防止缓存击穿
     * @param keyPrefix  key前缀
     * @param id         id值
     * @param type       查询对象的类对象
     * @param <R>        查询对象的类型
     * @param <ID>       id的类型
     * @param dbFallback 数据库查询逻辑，第一个泛型为参数类型，第二个泛型为返回值类型
     * @param time       时长
     * @param timeUnit   时间单位
     * @return
     */
    public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit timeUnit) {
        //1.得到key
        String key = keyPrefix + id;
        //2.从缓存中查找看是否命中
        String json = stringRedisTemplate.opsForValue().get(key);
        if (StrUtil.isNotBlank(json)) {
            //3.如果命中，且不为空字符串，则直接返回即可
            return JSONUtil.toBean(json, type);
        }

        //4.如果命中，还需要判断是否为空字符串，空字符串也直接返回,返回null
        if (json != null) {
            return null;
        }

        //5.如果未命中，即json为null时，需要重建缓存，查询数据库
        R r = dbFallback.apply(id);
        //6.数据库中没有该数据，把改id作为key记录到缓存中，且值为""(空字符串)
        if (r == null) {
            stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }

        //7.存在，写入redis当中
        this.set(key, r, time, timeUnit);
        return r;
    }

    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);//新建一个线程池

    /**
     * 缓存逻辑删除
     * @param keyPrefix 缓存key前缀
     * @param id 数据id，用于和前缀进行拼接形成一个独一无二的key
     * @param type 数据类型
     * @param dbFallback sql实现逻辑
     * @param lockKeyPrefix 互斥锁key前缀
     * @param time 过期时间
     * @param unit 时间单位
     * @param <R> 返回值类型
     * @param <ID> 数据Id类型
     * @return
     */
    public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type,
                                            Function<ID,R> dbFallback,
                                            String lockKeyPrefix,
                                            Long time, TimeUnit unit
                                            ) {
        //1.先从缓存中查询数据
        String key = keyPrefix + id;
        String json = stringRedisTemplate.opsForValue().get(key);

        //2.如果缓存中没有该数据，即缓存未命中，则直接返回null即可，
        // 因为这些缓存都是永久的，不会过期的，一般都会提前加载
        if (StrUtil.isBlank(json)) {
            return null;
        }

        //3.如果缓存命中，进行反序列化操作
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        R r = JSONUtil.toBean((JSONObject)redisData.getData(), type);
        LocalDateTime expireTime = redisData.getExpireTime();//获取过期时间

        //4.判断是否过期，没有过期返回缓存中的信息即可
        if(LocalDateTime.now().isBefore(expireTime)){
            return r;
        }

        //5.如果过期，则尝试获取互斥锁
        String lockKey = lockKeyPrefix + id;
        boolean isLock = tryLock(lockKey);
        if(isLock){
            //6.如果获取成功，则开启一个独立的线程，重建数据库
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    //查询数据库
                    R r1 = dbFallback.apply(id);
                    //重建缓存
                    this.setWithLogicalExpire(key,r1,time,unit);
                } catch (Exception e) {
                    throw new RuntimeException();
                }finally {
                    //释放锁
                    unlock(lockKey);
                }
            });
        }
        //返回旧数据
        return r;
    }

    /**
     * 尝试获取锁，如果获取成功返回
     * @param key 锁的key
     * @return
     */
    public boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10L, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    /**
     * 释放锁
     * @param key
     */
    public void unlock(String key){
        stringRedisTemplate.delete(key);
    }

}
